/******************************************************************************/
package jamie.org.sil.www.usxUtils;

import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/******************************************************************************/
/**
 * A class to parse text strings which include Bible references.
 * <p>
 * 
 * The class generates a {@link ParsedText} instance.  This is a list of
 * individual {@link ParsedTextElement}s, which between them represent the
 * original input, but which reflect more accurately the individual elements
 * which make up the input.
 * <p>
 * 
 * A number of classes inherit from ParsedTextElement, and it is these which
 * provide specific information about the content of the original text.
 * Specifically, there is a class to represent numbers, one to represent book
 * names, one to represent Bible references, one to represent the separators
 * used within Bible references, and one to represent "other" text.
 * <p>
 * 
 * The processing here begins by splitting the original text string so as
 * to produce a list made up of alternating text strings and numbers.  (If
 * the input text contains no numbers, then this list will comprise a single
 * text element.)
 * <p>
 * 
 * <b>IMPORTANT</b>: Note that all processing here assumes that the references
 * are held in <i>vernacular</i> form: full or vernacular book names, and
 * numerals using vernacular characters.  Note also that where you have multi-
 * word book names, I rely on them following the information supplied in the
 * configuration files <i>exactly</i>, so that if the configuration files give
 * the elements split up by single spaces, that's the way things must appear in
 * the input too.
 * 
 * We now carry out a series of progressive refinements ...
 * <p>
 * 
 * First, we try to locate book names in the text.  This is slightly more
 * complicated than might at first sight appear to be the case, because some
 * book names begin with numbers, and we've just split the numbers out.  We
 * therefore have to begin by isolating cases where a number, prepended to
 * the following text stream, gives us a book name -- in which case we drop
 * the number element, and instead create a book name entry.  Apart from this,
 * we look for places where text strings <i> end with book names, because these,
 * in conjunction with the following number might constitute a Bible reference
 * (although of course may not always do so).  At the end of this process,
 * the list of elements will have been modified to contain book name elements
 * at appropriate places.
 * <p>
 * 
 * Next, we look to see if any of the numbers is immediately followed by
 * something which could be a subverse marker.  This is a somewhat thankless
 * task, since there appears to be no consensus even over how this should be
 * handled in <i>English</i>, and certainly none when it comes to vernaculars.
 * However, in the absence of anything better, I assume that if a number is
 * immediately followed by a lower case letter which is itself not part of a
 * larger word, that letter is in fact a subverse marker.  The "number" element
 * is used to hold <i>any</i> number, but nonetheless its primary purpose is
 * to cater for numbers which form part of references, and it therefore makes
 * provision to hold a subverse marker.  This means I can move the subverse
 * marker out of the following text item and include it with the number.
 * (Clearly there are cases where this may be the wrong thing to so.  For
 * example, if the supposed subverse marker immediately follows a chapter
 * number, we probably have a syntax error, and ought to report it.  However
 * things are quite complicated enough without worrying about that.)
 * <p>
 *
 * Following this, we look for separators in the text.  To be recognised as a
 * separator, an item must appear in the list of separators in the configuration
 * data, and must appear between two numbers, or between a number and a book
 * name.  I make allowance for two separate sets of separators to be used, 
 * one for internal purposes and one for display purposes (although there is
 * no requirement that the two be distinct, so long as no confusion between the
 * two is possible), and I recognise either.
 * <p>
 * 
 * We now move on to the most complicated part: that of locating references
 * within the text.  I start off with the most explicit ones -- a book name
 * followed by a number, a chapter-and-verse-separator and another verse.
 * These elements are combined and replaced by a single Bible reference
 * element.
 * <p>
 * 
 * Next, I look to see if there are any book name elements left after the above,
 * which are immediately followed by numbers.  These I take as being references
 * to chapters, rather than to verses.  (Except, that is, for books like Jude,
 * which have only one chapter, and therefore tend to appear as single verse
 * numbers.  There's special processing to handle these.)
 * <p>
 * 
 * Next, I look for number followed by chapter-and-verse separator followed by
 * number.  These I take as full references to locations in the current book,
 * unless we are in the middle of a series of consecutive references, in which
 * case I take them as referring to whichever book was identified most recently
 * in that list.
 * <p>
 * 
 * And then finally, I look for isolated numbers which form part of a series
 * of consecutive references.  These I take as verse references to the most
 * recently-identified chapter and book in that series (unless the book is
 * something like Jude, in which case special processing is needed once again).
 * <p>
 * 
 * There are some special cases where the appropriate course of action is not
 * entirely clear.  For example, if I have a single number in isolation, is
 * that just a number, of is it a chapter reference to the current book, or
 * a verse reference to the current chapter?  I have opted to treat it just
 * as a number.  Similarly, an isolated number/range-separator/number I also
 * treat as plain text, not as some form of reference.
 * <p>
 * 
 * After all of this, I also validate that references are to valid chapters
 * and verses if the configuration information requires that I should do so.
 * <p>
 * 
 * It will be apparent from the above discussion that there are a huge number
 * of permutations to be handled, and I certainly cannot guarantee that I
 * have identified all of them, nor that I am processing everything correctly;
 * nor that I am identifying errors in an entirely rigorous manner.
 * <p>
 *
 *
 * @author ARA "Jamie" Jamieson
**/

public class BibleReferenceParser
{
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                 Public                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/


  /****************************************************************************/
  /** Singleton.

      @return instance
   */

  public static BibleReferenceParser instance ()
  {
    if (null == m_Instance) m_Instance = new BibleReferenceParser();
    return m_Instance;
  }


  /****************************************************************************/
  /**
   * Parses textual input and returns a structure which contains text and
   * references as appropriate.
   * 
   * @param text Text to be parsed.
   * 
   * @param dfltBookName Default book name to be used on references which lack one.
   * 
   * @return Structure containing text and references.
   */
  
  public ParsedText parse (String text,
                           String dfltBookName,
                           NumeralProcessor numProc)
  {
    /**************************************************************************/
    m_DfltBookName = dfltBookName;
    splitAtNumberBoundaries(text);
    if (1 == m_ParsedText.m_Tokens.size()) return m_ParsedText;
    findBooks();
    extractSubverses();
    findSeparators();
    generateParsedText(numProc);
    setAdjacentSeparatorCodesInBibleRefs();
    if (Config.instance().getCheckVersification()) validateReferences();
    return m_ParsedText;
  }

  
  
  /****************************************************************************/
 
  
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                 Private                                **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Constructor. */
  
  private BibleReferenceParser ()
  {
  }

  
  /****************************************************************************/
  /* Extract subverses.
   * 
   * Much of the processing we do here relies upon being able to locate
   * text strings which consist of nothing but separators (perhaps preceded or
   * followed by spaces).  If we have subverse markers in text strings, this
   * processing will be thwarted.
   * 
   * The basic idea here, therefore, is to identify likely candidates for
   * subverses, and move them into the "number" element which precedes them.
   * 
   * I regard things as being likely candidates where we have a number element
   * immediately followed by a text string, and the text string starts with
   * a single lower-case letter, followed either by non-word characters or by
   * nothing at all.
   * 
   * It will probably be apparent that this is very far from foolproof ...
   * 
   * * To state the blindingly obvious, it only makes sense to look for a
   *   subverse marker if the number preceding it is actually fulfilling
   *   the role of a verse, and we haven't yet established that.  (Later
   *   processing does its best to accommodate the possibility that I've
   *   made the wrong call here, but it probably won't cope with all
   *   possibilities.)
   *
   * * In some vernaculars, there may not be such things as lower case letters.
   * 
   * * There's no guarantee that a lower case letter following a number means
   *   that we have a subverse in all vernaculars -- it might just be a lower
   *   case letter, rather than a subverse marker.
   * 
   * * Conversely, there may be some vernaculars in which a subverse is marked
   *   in some other way.
   * 
   * There's no way I can address all the possibilities (not least because I
   * don't actually know what all the possibilities are), but this routine
   * represents my best effort in this direction. */
  
  private void extractSubverses ()
  {
    for (int i = 0; i < m_ParsedText.m_Tokens.size() - 1; ++i)
      if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementNumber &&
          m_ParsedText.m_Tokens.get(i + 1) instanceof ParsedTextElementString)          
      {
        Matcher matcher = m_SubversePattern1.matcher(m_ParsedText.m_Tokens.get(i + 1).toString());
        if (matcher.matches())
        {
          m_ParsedText.m_Tokens.get(i + 1).setValue(matcher.group(2));
          m_ParsedText.m_Tokens.get(i).setSubverse(matcher.group(1));
        }
        else
        {
          matcher = m_SubversePattern2.matcher(m_ParsedText.m_Tokens.get(i + 1).toString());
          if (matcher.matches())
          {
            m_ParsedText.m_Tokens.get(i).setSubverse(matcher.group(1));
            m_ParsedText.m_Tokens.remove(i + 1);
          }
        }
      }
  }

    
    
  /****************************************************************************/
  /**
     Check whether a given text string ends with the name of a book (case-
     insensitive check).  Note that I rely on the booknames arrays being
     in ascending order of string-length, so as to avoid picking up spurious
     matches where one book name (perhaps an abbreviated one) is a subset of
     another.  I also rely on s containing the book name _exactly_ as in
     the booknames array (except that I do a case-insensitive comparison) --
     ie if the name contains internal delimiters such as spaces, you need to
     have exactly the same delimiters in both cases.  If the book name contains
     two separate single embedded spaces, for example, then so must s.
   
     The return value is a two-element string giving the name of the book
     (in "correct" case) and s with this stripped out.  Any spaces in s
     _before_ the book name are retained. */

  private String[] findBookAtEndOfString (String s)
  {
    if (s.length() < 2) return null;
    String ss = s.trim().toLowerCase();
    for (int i = m_Support.getBookNamesInAscendingLengthOrderLowerCase().length - 1; i >= 0; --i)
      if (ss.endsWith(m_Support.getBookNamesInAscendingLengthOrderLowerCase()[i]))
      {
        String res[] = new String[2];
        res[0] = m_Support.getBookNamesInAscendingLengthOrderCorrectCase()[i];
        res[1] = s.replaceAll("\\p{Z}+$", "");
        res[1] = res[1].substring(0, res[1].length() - res[0].length());
        return res;
      }

    return null;
  }


  /****************************************************************************/
  /**
     Check whether a given text string starts with the name of a book (case-
     insensitive check).  Note that I rely on the booknames arrays being
     in ascending order of string-length, so as to avoid picking up spurious
     matches where one book name (perhaps an abbreviated one) is a subset of
     another.  I also rely on s containing the book name _exactly_ as in
     the booknames array (except that I do a case-insensitive comparison) --
     ie if the name contains internal delimiters such as spaces, you need to
     have exactly the same delimiters in both cases.  If the book name contains
     two separate single embedded spaces, for example, then so must s.
   
     The return value is a two-element string giving the name of the book
     (in "correct" case) and s with this stripped out.  Any spaces in s
     _after_ the book name are retained. */

  private String[] findBookAtStartOfString (String s)
  {
    if (s.length() < 2) return null;
    String ss = s.trim().toLowerCase();
    for (int i = m_Support.getBookNamesInAscendingLengthOrderLowerCase().length - 1; i >= 0; --i)
      if (ss.startsWith(m_Support.getBookNamesInAscendingLengthOrderLowerCase()[i]))
      {
        String res[] = new String[2];
        res[0] = m_Support.getBookNamesInAscendingLengthOrderCorrectCase()[i];
        res[1] = s.replaceAll("^\\p{Z}+", "");
        res[1] = res[1].substring(res[0].length());
        return res;
      }

    return null;
  }


  /****************************************************************************/
  /* Retokenises so as to indicate where book names appear in the text. */
  
  private void findBooks ()
  {
    /**************************************************************************/
    /* Look to see if we have any book-names like "1 Cor", which start with
     * numbers.  To identify these, we have to see if, by concatenating the
     * preceding number with the start of the present text, we end up with
     * a text string which starts with a full or abbreviated vernacular name.
     */
    
    for (int i = m_ParsedText.m_Tokens.size() - 1; i > 0; --i)
      if (m_ParsedText.m_Tokens.get(i - 1) instanceof ParsedTextElementNumber)
      {
        String s = m_ParsedText.m_Tokens.get(i - 1).toString() + m_ParsedText.m_Tokens.get(i).toString();
        String[] parsedBookDetails = findBookAtStartOfString(s);
        if (null != parsedBookDetails)
        {
          m_ParsedText.m_Tokens.set(i - 1, new ParsedTextElementBookName(parsedBookDetails[0]));
          m_ParsedText.m_Tokens.get(i).setValue(parsedBookDetails[1]);
        }
      }
    
    
    /**************************************************************************/
    /* Look to see if any text entries end with book names. */
    
    for (int i = m_ParsedText.m_Tokens.size() - 1; i >= 0; --i)
      if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementString)
      {
        String[] parsedBookDetails = findBookAtEndOfString(m_ParsedText.m_Tokens.get(i).toString());
        if (null != parsedBookDetails)
        {
	  m_ParsedText.m_Tokens.get(i).setValue(parsedBookDetails[1]); // Revised text.
          m_ParsedText.m_Tokens.add(i + 1, new ParsedTextElementBookName(parsedBookDetails[0])); // Book name.
        }
      }
  }
  
  
  /****************************************************************************/
  /* Retokenises so as to replace any text string which comprises _just_
   * delimiters and space, and which are preceded by numbers or followed by
   * numbers or book names.  This isn't totally foolproof -- you might have a
   * delimiter followed by a book name which is not, itself, part of a
   * reference, but we'll have to live with that. */
  
  private void findSeparators ()
  {
    for (int i = 1; i < m_ParsedText.m_Tokens.size() - 1; ++i)
      if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementString &&
          m_ParsedText.m_Tokens.get(i - 1) instanceof ParsedTextElementNumber &&
          (m_ParsedText.m_Tokens.get(i + 1) instanceof ParsedTextElementNumber ||
           m_ParsedText.m_Tokens.get(i + 1) instanceof ParsedTextElementBookName))
      {
        int sepType = m_Support.isSeparator(m_ParsedText.m_Tokens.get(i).getValue());
        if (sepType >= 0)
        {
          ParsedTextElementSeparator token = new ParsedTextElementSeparator(m_ParsedText.m_Tokens.get(i).getValue());
          token.setSeparatorCode(sepType);
          m_ParsedText.m_Tokens.set(i, token);
        }
      }
    }
    
    
  /****************************************************************************/
  /* Generates the "proper" version of the output.  This entails running
   * through the stuff we've generated to date, updating token types to
   * indicate the role played by the various items.
   * 
   * One important point to note.  We make provision internally to distinguish
   * between separators for ranges which lie entirely within a given chapter,
   * and ranges which span chapters; and similarly, we make provision for
   * multiple consecutive references in the same chapter and in different
   * chapters.
   * 
   * It might seem at first sight that this would provide important information
   * to help us distinguish between some of the different cases below.  However,
   * we cannot rely on this, because although the internal processing is capable
   * of making this distinction, we do not force the user to employ different
   * separators.  If, for instance, they want to use the same range separator
   * in both cases, they are at liberty to do so, and we cannot therefore
   * make the distinction readily ourselves.
   *
   * There is one other complication, which is that with books like Jude,
   * we don't actually _have_ chapters anyway, because there's only one
   * chapter, and the numbers we see are therefore verse references.  I leave
   * it to the Bible reference class to take care of that.
   * 
   * There are doubtless all kinds of special cases which the processing below
   * does not handle; various invalid inputs which it accepts; and various valid
   * inputs which it rejects.  We will have to hone the code over time -- it is
   * just too complex a task to be confident that it is completely correct (and
   * perhaps too complex _ever_ to have that confidence). */
  
  private void generateParsedText (NumeralProcessor numProc)
  {
    /**************************************************************************/
    /* We deal with the most fully specified items first.  Explicit book name,
     * followed by chapter, followed by chapter/verse separator, followed by
     * verse. */
    
    for (int i = m_ParsedText.m_Tokens.size() - 4; i >= 0; --i)
      if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementBookName &&
          m_ParsedText.m_Tokens.get(i + 1) instanceof ParsedTextElementNumber &&
          -1 != m_Support.isChapterAndVerseSeparator((m_ParsedText.m_Tokens.get(i + 2).toString())) &&
          m_ParsedText.m_Tokens.get(i + 3) instanceof ParsedTextElementNumber)
      {
        m_ParsedText.m_Tokens.set
          (i, new ParsedTextElementBibleRef
                    (m_ParsedText.m_Tokens.get(i).toString(), false,
                     m_ParsedText.m_Tokens.get(i + 1).toString(), false,
                     m_ParsedText.m_Tokens.get(i + 2).toString(), m_ParsedText.m_Tokens.get(i + 2).getSeparatorCode(),
                     m_ParsedText.m_Tokens.get(i + 3).toString(), false,
                     m_ParsedText.m_Tokens.get(i + 3).getSubverse()));
        m_ParsedText.m_Tokens.remove(i + 1);
        m_ParsedText.m_Tokens.remove(i + 1);
        m_ParsedText.m_Tokens.remove(i + 1);
      }
    
    
    
    /**************************************************************************/
    /* Assume now that if we have an explicit book name, followed by a number,
     * then the number must represent a reference to a chapter in this book. */
    
    for (int i = m_ParsedText.m_Tokens.size() - 2; i >= 0; --i)
      if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementBookName &&
          m_ParsedText.m_Tokens.get(i + 1) instanceof ParsedTextElementNumber)
      {
        m_ParsedText.m_Tokens.set
          (i, new ParsedTextElementBibleRef
                    (m_ParsedText.m_Tokens.get(i).toString(), false,
                     m_ParsedText.m_Tokens.get(i + 1).toString(), false,
                     "", -1,
                     numProc.toVernacular(1), true, 
                     ""));
        m_ParsedText.m_Tokens.remove(i + 1);
      }
    
    
    
    /**************************************************************************/
    /* Number followed by chapter/verse separator, followed by number.  We
     * assume this is a chapter and verse reference with an implicit book
     * name.  To find out the name, we need to work back from this point,
     * until either we hit a text element (in which case the book is the
     * one we are currently processing) or until we hit a reference (in which
     * case the book is the one associated with that reference). */
    
    for (int i = m_ParsedText.m_Tokens.size() - 3; i >= 0; --i)
      if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementNumber &&
          -1 != m_Support.isChapterAndVerseSeparator(m_ParsedText.m_Tokens.get(i + 1).toString()) &&
          m_ParsedText.m_Tokens.get(i + 2) instanceof ParsedTextElementNumber)
      {
        String dfltBookName = m_DfltBookName;
        for (int j = i - 1; j >= 0; --j)
        {
          if (m_ParsedText.m_Tokens.get(j) instanceof ParsedTextElementString) break;
          if (m_ParsedText.m_Tokens.get(j) instanceof ParsedTextElementBibleRef)
          {
            dfltBookName = ((ParsedTextElementBibleRef) m_ParsedText.m_Tokens.get(j)).getVernacularBookName();
            break;
          }
        }
      
        m_ParsedText.m_Tokens.set
          (i, new ParsedTextElementBibleRef
                    (dfltBookName, true,
                     m_ParsedText.m_Tokens.get(i).toString(), false,
                     m_ParsedText.m_Tokens.get(i + 1).toString(), m_ParsedText.m_Tokens.get(i + 1).getSeparatorCode(),
                     m_ParsedText.m_Tokens.get(i + 2).toString(), false,
                     m_ParsedText.m_Tokens.get(i + 2).getSubverse()));
        m_ParsedText.m_Tokens.remove(i + 1);
        m_ParsedText.m_Tokens.remove(i + 1);
      }
    
    
    
    /**************************************************************************/
    /* We now repeatedly look either for things of the form ...
     * 
     *   ref / chapterVerseSeparator / number   or
     *   ref / multiSeparator / number
     *
     * and carry on doing this until we no longer make any further changes.
     * 
     * In both cases ...
     *
     * * If the reference contains a defaulted verse, it must have been a
     *   chapter reference, and so we assume the present number is a chapter
     *   too -- in which case, we default its verse number to 1.
     * 
     * * If the reference contains a non-defaulted verse, we are currently
     *   looking at a verse number, and we need to take the chapter number from
     *   the reference (and mark it as defaulted).
     * 
     * * There are some slight complications with books like Jude which have
     *   only one chapter, and are therefore normally given as verse-only
     *   references, but these are taken care of within ParsedTextBibleRed.
     */
    
    boolean madeChanges = true;
    while (madeChanges)
    {
      madeChanges = false;
      for (int i = 2; i < m_ParsedText.m_Tokens.size(); ++i)
        if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementNumber &&
            -1 != m_Support.isRangeSeparator(m_ParsedText.m_Tokens.get(i - 1).getSeparatorCode()) &&
            m_ParsedText.m_Tokens.get(i - 2) instanceof ParsedTextElementBibleRef)
        {
          ParsedTextElementBibleRef ref = (ParsedTextElementBibleRef) m_ParsedText.m_Tokens.get(i - 2);
          String bookName = ref.getVernacularBookName();
          madeChanges = true;
          if (ref.getVerseDefaulted())
            m_ParsedText.m_Tokens.set
              (i, new ParsedTextElementBibleRef
                        (bookName, true,
                         m_ParsedText.m_Tokens.get(i).toString(), false,
                         "", -1,
                         numProc.toVernacular(1), true, 
                         ""));
          else
            m_ParsedText.m_Tokens.set
              (i, new ParsedTextElementBibleRef
                        (bookName, true,
                         ref.getChapter(), true,
                         "", -1,
                         m_ParsedText.m_Tokens.get(i).toString(), false,
                         m_ParsedText.m_Tokens.get(i).getSubverse()));
        }
    
    
    
      /************************************************************************/
      for (int i = 2; i < m_ParsedText.m_Tokens.size(); ++i)
        if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementNumber &&
            -1 != m_Support.isMultiSeparator(m_ParsedText.m_Tokens.get(i - 1).getSeparatorCode()) &&
            m_ParsedText.m_Tokens.get(i - 2) instanceof ParsedTextElementBibleRef)
        {
          ParsedTextElementBibleRef ref = (ParsedTextElementBibleRef) m_ParsedText.m_Tokens.get(i - 2);
          String bookName = ref.getVernacularBookName();
          madeChanges = true;
          if (ref.getVerseDefaulted())
            m_ParsedText.m_Tokens.set
              (i, new ParsedTextElementBibleRef
                        (bookName, true,
                         m_ParsedText.m_Tokens.get(i).toString(), false,
                         "", -1,
                         numProc.toVernacular(1), true, 
                         ""));
          else
            m_ParsedText.m_Tokens.set
              (i, new ParsedTextElementBibleRef
                         (bookName, true,
                         ref.getChapter(), true,
                         "", -1,
                         m_ParsedText.m_Tokens.get(i).toString(), false,
                         m_ParsedText.m_Tokens.get(i).getSubverse()));
        }
    }
  }
  
  
  /****************************************************************************/
  /* Where a Bible reference is preceded or followed by a separator, make a note
   * in the reference itself of what the separator is.  This may be useful for
   * example where we are handling a range and don't want to generate a
   * hyperlink for the reference which marks the end of the range.
   */
  
  private void setAdjacentSeparatorCodesInBibleRefs ()
  {
    for (int i = 1; i < m_ParsedText.m_Tokens.size(); ++i)
      if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementBibleRef &&
          m_ParsedText.m_Tokens.get(i - 1) instanceof ParsedTextElementSeparator)
        m_ParsedText.m_Tokens.get(i).setPreviousSeparatorCode(m_ParsedText.m_Tokens.get(i - 1).getSeparatorCode());

    
    for (int i = 0; i < m_ParsedText.m_Tokens.size() - 1; ++i)
      if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementBibleRef &&
          m_ParsedText.m_Tokens.get(i + 1) instanceof ParsedTextElementSeparator)
        m_ParsedText.m_Tokens.get(i).setNextSeparatorCode(m_ParsedText.m_Tokens.get(i + 1).getSeparatorCode());
  }
  
  
  /****************************************************************************/
  /* Split at the points where numbers appear.  Note that regardless of what
   * the input text looks like, we're guaranteed to have a plain text
   * chunk at the beginning and the end, although one or both may be empty. */
  
  private void splitAtNumberBoundaries (String text)
  {
    m_ParsedText = new ParsedText();
    ArrayList<String> tokens = splitWithDelims(m_NumberPattern, text);
    
    for (int i = 0; i < tokens.size(); ++i)
    {
      if (0 == (i & 1))
        m_ParsedText.m_Tokens.add(new ParsedTextElementString(tokens.get(i)));
      else
        m_ParsedText.m_Tokens.add(new ParsedTextElementNumber(tokens.get(i)));
    }
  }
    
    
    
  /****************************************************************************/
  /* Splits a string at delimiters matching the given pattern, returning an
   * ArrayList which contains both the "text" and the delimiters, suitably
   * interleaved.  The output always starts and ends with a text entry; if the
   * input starts or ends with a delimiter, the first or last text entry is an
   * empty string.
   */
  
  private ArrayList<String> splitWithDelims (Pattern pattern, String text)
  {
    int lastMatch = 0;
    ArrayList<String> splitted = new ArrayList<String>();
    Matcher m = pattern.matcher(text);

    while (m.find())
    {
      splitted.add(text.substring(lastMatch, m.start()));
      splitted.add(m.group());
      lastMatch = m.end();
    }

    splitted.add(text.substring(lastMatch));

    return splitted;
  }
  
 
  /****************************************************************************/
  /* Does what it says on the tin. */
  
  private void validateReferences ()
  {
    for (int i = 0; i <= m_ParsedText.m_Tokens.size(); ++i)
      if (m_ParsedText.m_Tokens.get(i) instanceof ParsedTextElementBibleRef)
      {
        String s = ((ParsedTextElementBibleRef) m_ParsedText.m_Tokens.get(i)).validate();
        if (0 != s.length())
          LogManager.instance().warning(s);
      }
  }
  
  
  
  /****************************************************************************/
  private static final Pattern m_NumberPattern = Pattern.compile("\\p{N}+");
  private static final Pattern m_SubversePattern1 = Pattern.compile("^(\\p{L})([^\\p{L}]+)");
  private static final Pattern m_SubversePattern2 = Pattern.compile("^(\\p{L})$");
  
  private static BibleReferenceParser m_Instance = null;
  
  private String m_DfltBookName = null;
  private ParsedText m_ParsedText = null;
  BibleReferenceSupport m_Support = BibleReferenceSupport.instance();
}
